Tower
The documentation in this section is for users that require further details on the individual Tailslide Tower component.
Overview
Tower is a full-stack application that handles the functionality related to feature flag management.
It consists of a React user interface that allows a user to perform basic CRUD functionality, such as creating an app and flag, making edits to flags, toggling a flag on and off, and viewing logs related to those flags.
Tower’s backend is written in Node.js and Express. Tower is configured to connect to the database, and upon any change to the feature flag data, Tower updates the database and publishes the full set of feature flag ruleset data to NATS JetStream.
Running Tower Locally
Clone main branch of repository here
Sample .env
file to add into the server directory
PORT=3001
PGHOST='localhost'
PGUSER='postgres'
PGDATABASE='tower'
PGPASSWORD='secret'
PGPORT=5432
NATS_SERVER='nats://127.0.0.1:4222'
SDK_KEY='myToken'
REDIS_SERVER='{"socket":{"host":"localhost"}}'
Sample .env
file to add into the client directory
REACT_APP_NATS_WS_SERVER='ws://0.0.0.0:8080'
REACT_APP_SDK_KEY='myToken'
Start NATS Jetstream
Install NATS
From Tower root directory in separate terminal run nats-server -c nats.conf
to stop a nats server run nats-server --signal quit
to delete stream messages run nats stream purge
Start Redis Time Series Database
Install Redis
From any directory in separate terminal run docker run -p 6379:6379 -it --rm redislabs/redistimeseries
Start PostgreSQL Database
Install PostgreSQL
From any directory run brew services start postgresql
Start Backend
From the server directory npm install
From the server directory run npm run dev
The backend app is now available at http://localhost:3001
Start Frontend
From the server directory npm install
From the server directory run npm start
The frontend app is now available at http://localhost:3000
API Endpoints
All requests and responses are in json
Apps
GET /apps
Returns all apps
Example Response:
{
"payload": [
{
"id": 1,
"title": "First app",
"created_at": "2022-08-11T18:13:55.106Z",
"updated_at": "2022-08-11T18:13:55.106Z"
},
{
"id": 2,
"title": "Second App",
"created_at": "2022-08-11T18:27:17.068Z",
"updated_at": "2022-08-11T18:27:17.068Z"
}
]
}
GET apps/:appId
Returns the app with a matching appId
Example Response:
{
"payload": {
"id": 2,
"title": "Second App",
"created_at": "2022-08-11T18:27:17.068Z",
"updated_at": "2022-08-11T18:27:17.068Z"
}
}
POST /apps
Creates a new app
Example Request:
{
"title": "First App"
}
Example Response:
{
"payload": {
"title": "First App",
"id": 1,
"created_at": "2022-08-11T18:27:17.068Z"
}
}
PATCH /apps/:appId
Updates the app with a matching appId
Example Request:
{
"title": "App with Edited name"
}
Example Response:
{
"payload": {
"id": 2,
"title": "App with Edited name",
"created_at": "2022-08-11T18:27:17.068Z",
"updated_at": "2022-08-11T18:42:19.981Z"
}
}
DELETE /apps/:appId
Deletes the app with a matching appId
Example Response:
{
"payload": {
"id": 2,
"title": "App with Edited name",
"created_at": "2022-08-11T18:27:17.068Z",
"updated_at": "2022-08-11T18:42:19.981Z"
}
}
Flags
GET /apps/:appId/flags
Returns all flags ruleset data belonging to a specific app
Example Response:
{
"payload": [
{
"id": 1,
"app_id": 1,
"title": "App 1 Flag 1",
"description": "",
"is_active": false,
"rollout_percentage": 100,
"white_listed_users": "",
"error_threshold_percentage": 50,
"circuit_status": "open",
"is_recoverable": false,
"circuit_recovery_percentage": 100,
"circuit_recovery_delay": 100,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 100,
"circuit_recovery_increment_percentage": 0.1,
"circuit_recovery_profile": "linear",
"webhooks": "",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T18:51:52.882Z"
},
{
"id": 2,
"app_id": 1,
"title": "App 1 Flag 2",
"description": "",
"is_active": false,
"rollout_percentage": 100,
"white_listed_users": "",
"error_threshold_percentage": 50,
"circuit_status": "open",
"is_recoverable": false,
"circuit_recovery_percentage": 100,
"circuit_recovery_delay": 100,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 100,
"circuit_recovery_increment_percentage": 0.1,
"circuit_recovery_profile": "linear",
"webhooks": "",
"created_at": "2022-08-11T18:52:27.200Z",
"updated_at": "2022-08-11T18:52:27.200Z"
}
]
}
GET /flags/:flagId
Returns the flag with a matching flagId
and its ruleset data
Example Response:
{
"payload": {
"id": 1,
"title": "App 1 Flag 1",
"app_id": 1,
"is_active": false,
"description": "",
"rollout_percentage": 100,
"white_listed_users": "",
"webhooks": "",
"circuit_status": "open",
"is_recoverable": false,
"error_threshold_percentage": 50,
"circuit_recovery_percentage": 100,
"circuit_recovery_delay": 100,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 100,
"circuit_recovery_increment_percentage": 0.1,
"circuit_recovery_profile": "linear",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T18:51:52.882Z",
"logs": [
{
"log_id": 1,
"flag_id": 1,
"log_description": "Flag Created",
"action_type": "create",
"created_at": "2022-08-11T18:51:52.887Z",
"updated_at": "2022-08-11T18:51:52.887Z"
}
]
}
}
POST /apps/:appId/flags
Creates a new flag
Successfully creating a flag with POST
will also create a new log. All flags are initially created in an 'off' toggle state.
Example Request:
{
"title": "App 1 Flag 1",
"rollout_percentage": 100,
"circuit_recovery_percentage": 100,
"error_threshold_percentage": 50
}
Example Response:
{
"payload": {
"id": 1,
"app_id": 1,
"title": "App 1 Flag 1",
"description": "",
"is_active": false,
"rollout_percentage": 100,
"white_listed_users": "",
"error_threshold_percentage": 50,
"circuit_status": "open",
"is_recoverable": false,
"circuit_recovery_percentage": 100,
"circuit_recovery_delay": 100,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 100,
"circuit_recovery_increment_percentage": 0.1,
"circuit_recovery_profile": "linear",
"webhooks": "",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T18:51:52.882Z"
}
}
PATCH /flags/:flagId
Updates the flag with a matching flagId
Successfully updating a flag with PATCH
will create a new log.
Example Request:
{
"title": "App 1 Flag 1",
"rollout_percentage": 50,
"circuit_status": "close",
"is_active": false,
"is_recoverable": true,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_increment_percentage": 10,
"circuit_recovery_percentage": 10,
"error_threshold_percentage": 40,
"circuit_recovery_rate": 400,
"circuit_recovery_profile": "linear",
"circuit_recovery_delay": 8000,
"webhooks": "https://hooks.slack.com/services/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"
}
Example Response:
{
"payload": {
"id": 1,
"app_id": 1,
"title": "App 1 Flag 1",
"description": "",
"is_active": false,
"rollout_percentage": 50,
"white_listed_users": "",
"error_threshold_percentage": 40,
"circuit_status": "open",
"is_recoverable": true,
"circuit_recovery_percentage": 10,
"circuit_recovery_delay": 8000,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 400,
"circuit_recovery_increment_percentage": 10,
"circuit_recovery_profile": "linear",
"webhooks": "https://hooks.slack.com/services/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",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T18:56:12.441Z"
}
}
PATCH /flags/:flagId/toggle
Toggles the is_active
state of a flag with a matching flagId
on or off
Successfully toggling a flag with PATCH
will create a new log.
Example Response:
{
"payload": {
"id": 1,
"app_id": 1,
"title": "App 1 Flag 1",
"description": "",
"is_active": false,
"rollout_percentage": 50,
"white_listed_users": "",
"error_threshold_percentage": 40,
"circuit_status": "open",
"is_recoverable": true,
"circuit_recovery_percentage": 10,
"circuit_recovery_delay": 8000,
"circuit_initial_recovery_percentage": 10,
"circuit_recovery_rate": 400,
"circuit_recovery_increment_percentage": 10,
"circuit_recovery_profile": "linear",
"webhooks": "https://hooks.slack.com/services/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",
"created_at": "2022-08-11T18:51:52.882Z",
"updated_at": "2022-08-11T19:03:47.457Z"
}
}
DELETE /flags/:flagId
Deletes the flag with a matching flagId
Successfully deleting a flag with DELETE
will create a new log.
Example Response:
{
"payload": 1
}
POST /flags/circuit/:flagId/open
Trips the circuit of a flag with a matching flagId
open
Successfully opening a circuit with POST
will create a new log.
Example Response:
{
"payload": 1
}
POST /flags/circuit/:flagId/close
Trips the circuit of a flag with a matching flagId
closed
Successfully closing a circuit with POST
will create a new log.
Example Response:
{
"payload": 1
}
Logs
GET /logs
Returns all logs
Example Response:
{
"payload": [
{
"log_id": 4,
"flag_id": 1,
"flag_title": "App 1 Flag 1",
"log_description": "Circuit Tripped Open",
"action_type": "circuit_open",
"created_at": "2022-08-11T19:27:29.104Z",
"updated_at": "2022-08-11T19:27:29.104Z"
},
{
"log_id": 3,
"flag_id": 1,
"flag_title": "App 1 Flag 1",
"log_description": "Circuit Closed",
"action_type": "circuit_close",
"created_at": "2022-08-11T19:25:36.826Z",
"updated_at": "2022-08-11T19:25:36.826Z"
},
{
"log_id": 2,
"flag_id": 2,
"flag_title": "App 1 Flag 2",
"log_description": "Flag Created",
"action_type": "create",
"created_at": "2022-08-11T19:25:13.614Z",
"updated_at": "2022-08-11T19:25:13.614Z"
},
{
"log_id": 1,
"flag_id": 1,
"flag_title": "App 1 Flag 1",
"log_description": "Flag Created",
"action_type": "create",
"created_at": "2022-08-11T19:25:05.345Z",
"updated_at": "2022-08-11T19:25:05.345Z"
}
]
}
GET /logs/:appId
Returns all logs belonging to an app with a matching appId
Example Response:
{
"payload": [
{
"id": 1,
"flag_id": 1,
"app_id": 1,
"description": "Flag Created",
"action_type": "create",
"flag_title": "App 1 Flag 1",
"flag_description": "",
"created_at": "2022-08-11T19:25:05.345Z",
"updated_at": "2022-08-11T19:25:05.345Z"
},
{
"id": 3,
"flag_id": 1,
"app_id": 1,
"description": "Circuit Closed",
"action_type": "circuit_close",
"flag_title": "App 1 Flag 1",
"flag_description": "",
"created_at": "2022-08-11T19:25:36.826Z",
"updated_at": "2022-08-11T19:25:36.826Z"
}
]
}
Keys
GET /keys
Returns a SDK key
Example Response:
{
"payload": {
"id": "70a90b16-201c-4466-8dc9-5f5c620d5732",
"created_at": "2022-08-11T19:37:46.595Z",
"updated_at": "2022-08-11T19:37:46.595Z"
}
}
POST /keys
Creates a SDK key
Example Response:
{
"payload": {
"id": "3e381b25-7686-4847-b861-5ec9b87378f1",
"created_at": "2022-08-11T19:37:13.391Z",
"updated_at": "2022-08-11T19:37:13.391Z"
}
}
Redis Timeseries
GET /flags/:flagId/timeseries
Returns success/failure counts for a specified flag grouped into time buckets within a selected window of time
{
"payload": {
[
{ "timestamp": 1661264546556, "success": 137, "failure": 4 },
{ "timestamp": 1661264549556, "success": 134, "failure": 6 },
{ "timestamp": 1661264552556, "success": 138, "failure": 3 },
{ "timestamp": 1661264555556, "success": 136, "failure": 5 },
{ "timestamp": 1661264558556, "success": 131, "failure": 9 },
{ "timestamp": 1661264561556, "success": 135, "failure": 6 },
{ "timestamp": 1661264564556, "success": 130, "failure": 10 },
{ "timestamp": 1661264567556, "success": 130, "failure": 11 },
{ "timestamp": 1661264570556, "success": 137, "failure": 3 },
{ "timestamp": 1661264573556, "success": 132, "failure": 9 }
]
}
}